/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * PropertyPath.java
 * Copyright (C) 2006 University of Waikato, Hamilton, New Zealand
 */

package weka.core;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * A helper class for accessing properties in nested objects, e.g., accessing
 * the "getRidge" method of a LinearRegression classifier part of
 * MultipleClassifierCombiner, e.g., Vote. For doing so, one needs to supply the
 * object to work on and a property path. The property path is a dot delimited
 * path of property names ("getFoo()" and "setFoo(int)" have "foo" as property
 * name), indices of arrays are 0-based. E.g.:
 * <p/>
 * 
 * <code>getPropertyDescriptor(vote, "classifiers[1].ridge")</code> will return
 * the second classifier (which should be our LinearRegression) of the given
 * Vote meta-classifier and there the property descriptor of the "ridge"
 * property. <code>getValue(...)</code> will return the actual value of the
 * ridge parameter and <code>setValue(...)</code> will set it.
 * 
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 4742 $
 */
public class PropertyPath implements RevisionHandler {

	/**
	 * Represents a single element of a property path
	 * 
	 * @author fracpete (fracpete at waikato dot ac dot nz)
	 * @version $Revision: 4742 $
	 */
	public static class PathElement implements Cloneable, RevisionHandler {

		/** the property */
		protected String m_Name;

		/** the index of the array (-1 for none) */
		protected int m_Index;

		/**
		 * initializes the path element with the given property
		 * 
		 * @param property
		 *            the property to initialize with
		 */
		public PathElement(String property) {
			super();

			if (property.indexOf("[") > -1) {
				m_Name = property.replaceAll("\\[.*$", "");
				m_Index = Integer.parseInt(property.replaceAll(".*\\[", "")
						.replaceAll("\\].*", ""));
			} else {
				m_Name = property;
				m_Index = -1;
			}
		}

		/**
		 * returns a clone of the current object
		 * 
		 * @return the clone of the current state
		 */
		public Object clone() {
			return new PathElement(this.toString());
		}

		/**
		 * returns the name of the property
		 * 
		 * @return the name of the property
		 */
		public String getName() {
			return m_Name;
		}

		/**
		 * returns whether the property is an index-based one
		 * 
		 * @return true if the property has an index
		 */
		public boolean hasIndex() {
			return (getIndex() > -1);
		}

		/**
		 * returns the index of the property, -1 if the property is not an
		 * index-based one
		 * 
		 * @return the index of the property
		 */
		public int getIndex() {
			return m_Index;
		}

		/**
		 * returns the element once again as string
		 * 
		 * @return the property as string
		 */
		public String toString() {
			String result;

			result = getName();
			if (hasIndex())
				result += "[" + getIndex() + "]";

			return result;
		}

		/**
		 * Returns the revision string.
		 * 
		 * @return the revision
		 */
		public String getRevision() {
			return RevisionUtils.extract("$Revision: 4742 $");
		}
	}

	/**
	 * Contains a (property) path structure
	 * 
	 * @author fracpete (fracpete at waikato dot ac dot nz)
	 * @version $Revision: 4742 $
	 */
	public static class Path implements RevisionHandler {

		/** the structure */
		protected Vector m_Elements;

		/**
		 * default constructor, only used internally
		 */
		protected Path() {
			super();

			m_Elements = new Vector();
		}

		/**
		 * uses the given dot-path
		 * 
		 * @param path
		 *            path in dot-notation
		 */
		public Path(String path) {
			this();

			m_Elements = breakUp(path);
		}

		/**
		 * uses the vector with PathElement objects to initialize with
		 * 
		 * @param elements
		 *            the PathElements to use
		 */
		public Path(Vector elements) {
			this();

			for (int i = 0; i < elements.size(); i++)
				m_Elements.add(((PathElement) elements.get(i)).clone());
		}

		/**
		 * uses the given array as elements for the path
		 * 
		 * @param elements
		 *            the path elements to use
		 */
		public Path(String[] elements) {
			this();

			for (int i = 0; i < elements.length; i++)
				m_Elements.add(new PathElement(elements[i]));
		}

		/**
		 * breaks up the given path and returns it as vector
		 * 
		 * @param path
		 *            the path to break up
		 * @return the single elements of the path
		 */
		protected Vector breakUp(String path) {
			Vector result;
			StringTokenizer tok;

			result = new Vector();

			tok = new StringTokenizer(path, ".");
			while (tok.hasMoreTokens())
				result.add(new PathElement(tok.nextToken()));

			return result;
		}

		/**
		 * returns the element at the given index
		 * 
		 * @param index
		 *            the index of the element to return
		 * @return the specified element
		 */
		public PathElement get(int index) {
			return (PathElement) m_Elements.get(index);
		}

		/**
		 * returns the number of path elements of this structure
		 * 
		 * @return the number of path elements
		 */
		public int size() {
			return m_Elements.size();
		}

		/**
		 * returns a path object based on the given path string
		 * 
		 * @param path
		 *            path to work on
		 * @return the path structure
		 */
		public static Path parsePath(String path) {
			return new Path(path);
		}

		/**
		 * returns a subpath of the current structure, starting with the
		 * specified element index up to the end
		 * 
		 * @param startIndex
		 *            the first element of the subpath
		 * @return the new subpath
		 */
		public Path subpath(int startIndex) {
			return subpath(startIndex, size());
		}

		/**
		 * returns a subpath of the current structure, starting with the
		 * specified element index up. The endIndex specifies the element that
		 * is not part of the new subpath. In other words, the new path contains
		 * the elements from "startIndex" up to "(endIndex-1)".
		 * 
		 * @param startIndex
		 *            the first element of the subpath
		 * @param endIndex
		 *            the element that is after the last added element
		 * @return the new subpath
		 */
		public Path subpath(int startIndex, int endIndex) {
			Vector list;
			int i;

			list = new Vector();
			for (i = startIndex; i < endIndex; i++)
				list.add(get(i));

			return new Path(list);
		}

		/**
		 * returns the structure again as a dot-path
		 * 
		 * @return the path structure as dot-path
		 */
		public String toString() {
			String result;
			int i;

			result = "";

			for (i = 0; i < m_Elements.size(); i++) {
				if (i > 0)
					result += ".";
				result += m_Elements.get(i);
			}

			return result;
		}

		/**
		 * Returns the revision string.
		 * 
		 * @return the revision
		 */
		public String getRevision() {
			return RevisionUtils.extract("$Revision: 4742 $");
		}
	}

	/**
	 * A helper class that stores Object and PropertyDescriptor together.
	 * 
	 * @author fracpete (fracpete at waikato dot ac dot nz)
	 * @version $Revision: 4742 $
	 */
	protected static class PropertyContainer implements RevisionHandler {

		/** the descriptor */
		protected PropertyDescriptor m_Descriptor;

		/** the associated object */
		protected Object m_Object;

		/**
		 * initializes the container
		 * 
		 * @param desc
		 *            the property descriptor
		 * @param obj
		 *            the associated object
		 */
		public PropertyContainer(PropertyDescriptor desc, Object obj) {
			super();

			m_Descriptor = desc;
			m_Object = obj;
		}

		/**
		 * returns the stored descriptor
		 * 
		 * @return the stored descriptor
		 */
		public PropertyDescriptor getDescriptor() {
			return m_Descriptor;
		}

		/**
		 * returns the stored object
		 * 
		 * @return the stored object
		 */
		public Object getObject() {
			return m_Object;
		}

		/**
		 * Returns the revision string.
		 * 
		 * @return the revision
		 */
		public String getRevision() {
			return RevisionUtils.extract("$Revision: 4742 $");
		}
	}

	/**
	 * returns the property and object associated with the given path, null if a
	 * problem occurred.
	 * 
	 * @param src
	 *            the object to start from
	 * @param path
	 *            the path to follow
	 * @return not null, if the property could be found
	 */
	public static PropertyContainer find(Object src, Path path) {
		PropertyContainer result;
		PropertyDescriptor desc;
		Object newSrc;
		PathElement part;
		Method method;
		Object methodResult;

		// get descriptor
		part = path.get(0);
		try {
			desc = new PropertyDescriptor(part.getName(), src.getClass());
		} catch (Exception e) {
			desc = null;
			e.printStackTrace();
		}

		// problem occurred? -> stop
		if (desc == null)
			return null;

		// end of path reached?
		if (path.size() == 1) {
			result = new PropertyContainer(desc, src);
		}
		// recurse further
		else {
			try {
				method = desc.getReadMethod();
				methodResult = method.invoke(src, (Object[]) null);
				if (part.hasIndex())
					newSrc = Array.get(methodResult, part.getIndex());
				else
					newSrc = methodResult;
				result = find(newSrc, path.subpath(1));
			} catch (Exception e) {
				result = null;
				e.printStackTrace();
			}
		}

		return result;
	}

	/**
	 * returns the property associated with the given path, null if a problem
	 * occurred.
	 * 
	 * @param src
	 *            the object to start from
	 * @param path
	 *            the path to follow
	 * @return not null, if the property could be found
	 */
	public static PropertyDescriptor getPropertyDescriptor(Object src, Path path) {
		PropertyContainer cont;

		cont = find(src, path);

		if (cont == null)
			return null;
		else
			return cont.getDescriptor();
	}

	/**
	 * returns the property associated with the given path
	 * 
	 * @param src
	 *            the object to start from
	 * @param path
	 *            the path to follow
	 * @return not null, if the property could be found
	 */
	public static PropertyDescriptor getPropertyDescriptor(Object src,
			String path) {
		return getPropertyDescriptor(src, new Path(path));
	}

	/**
	 * returns the value specified by the given path from the object
	 * 
	 * @param src
	 *            the object to work on
	 * @param path
	 *            the retrieval path
	 * @return the value, null if an error occurred
	 */
	public static Object getValue(Object src, Path path) {
		Object result;
		PropertyContainer cont;
		Method method;
		Object methodResult;
		PathElement part;

		result = null;

		cont = find(src, path);
		// problem?
		if (cont == null)
			return null;

		// retrieve the value
		try {
			part = path.get(path.size() - 1);
			method = cont.getDescriptor().getReadMethod();
			methodResult = method.invoke(cont.getObject(), (Object[]) null);
			if (part.hasIndex())
				result = Array.get(methodResult, part.getIndex());
			else
				result = methodResult;
		} catch (Exception e) {
			result = null;
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * returns the value specified by the given path from the object
	 * 
	 * @param src
	 *            the object to work on
	 * @param path
	 *            the retrieval path
	 * @return the value, null if an error occurred
	 */
	public static Object getValue(Object src, String path) {
		return getValue(src, new Path(path));
	}

	/**
	 * set the given value specified by the given path in the object
	 * 
	 * @param src
	 *            the object to work on
	 * @param path
	 *            the retrieval path
	 * @param value
	 *            the value to set
	 * @return true if the value could be set
	 */
	public static boolean setValue(Object src, Path path, Object value) {
		boolean result;
		PropertyContainer cont;
		Method methodRead;
		Method methodWrite;
		Object methodResult;
		PathElement part;

		result = false;

		cont = find(src, path);
		// problem?
		if (cont == null)
			return result;

		// set the value
		try {
			part = path.get(path.size() - 1);
			methodRead = cont.getDescriptor().getReadMethod();
			methodWrite = cont.getDescriptor().getWriteMethod();
			if (part.hasIndex()) {
				methodResult = methodRead.invoke(cont.getObject(),
						(Object[]) null);
				Array.set(methodResult, part.getIndex(), value);
				methodWrite.invoke(cont.getObject(),
						new Object[] { methodResult });
			} else {
				methodWrite.invoke(cont.getObject(), new Object[] { value });
			}
			result = true;
		} catch (Exception e) {
			result = false;
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * set the given value specified by the given path in the object
	 * 
	 * @param src
	 *            the object to work on
	 * @param path
	 *            the retrieval path
	 * @param value
	 *            the value to set
	 */
	public static void setValue(Object src, String path, Object value) {
		setValue(src, new Path(path), value);
	}

	/**
	 * Returns the revision string.
	 * 
	 * @return the revision
	 */
	public String getRevision() {
		return RevisionUtils.extract("$Revision: 4742 $");
	}

	/**
	 * for testing only
	 * 
	 * @param args
	 *            the commandline options - ignored
	 * @throws Exception
	 *             if something goes wrong
	 */
	public static void main(String[] args) throws Exception {
		// Path
		Path path = new Path("hello.world[2].nothing");
		System.out.println("Path: " + path);
		System.out.println(" -size: " + path.size());
		System.out.println(" -elements:");
		for (int i = 0; i < path.size(); i++)
			System.out.println("  " + i + ". " + path.get(i).getName() + " -> "
					+ path.get(i).getIndex());

		/*
		 * // retrieving ridge with path weka.classifiers.meta.Vote vote = new
		 * weka.classifiers.meta.Vote(); vote.setClassifiers( new
		 * weka.classifiers.Classifier[]{ new weka.classifiers.trees.J48(), new
		 * weka.classifiers.functions.LinearRegression()}); path = new
		 * Path("classifiers[1].ridge"); System.out.println("path: " + path +
		 * " -> " + getValue(vote, path));
		 * 
		 * // setting ridge with path and retrieving it again setValue(vote,
		 * path.toString(), new Double(0.1)); System.out.println("path: " + path
		 * + " -> " + getValue(vote, path));
		 */
	}
}
